﻿//-----------------------------------------------------------------------
// <copyright file="ODataReader.cs" company="None">
//     None. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace WCFDataServiceFormatExtensions
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Net;
    using System.Security.Cryptography.X509Certificates;
    using System.Threading;
    using System.Xml;
    

    /// <summary>
    /// Implements Reader class for Atom feeds. 
    /// </summary>
    public class ODataReader
    {
        /// <summary>
        /// Data stream to be read.
        /// </summary>
        private XmlReader stream = null;

        /// <summary>
        /// Outer XML of Data node.
        /// </summary>
        private string nodeOuterXML;

        /// <summary>
        /// Local nage of current node.
        /// </summary>
        private string localName;

        /// <summary>
        /// Current line is being build.
        /// </summary>
        private string curLine = string.Empty;

        /// <summary>
        /// Temp doc to hold xml data node.
        /// </summary>
        private XmlDocument tempDoc;

        /// <summary>
        /// OData Namespaces holder.
        /// </summary>
        private DSNameSpaces ns;

        /// <summary>
        /// X509Certificate to be used for service request.
        /// </summary>
        private X509Certificate cert;

        /// <summary>
        /// HttpWebRequest instance to hold current request.
        /// </summary>
        private HttpWebRequest webReq = null;

        /// <summary>
        /// HttpWebResponse instance to hold current responce.
        /// </summary>
        private HttpWebResponse webresp = null;

        /// <summary>
        /// Initializes a new instance of the ODataReader class.
        /// </summary>
        /// <param name="url">URL to be downloded.</param>
        /// <param name="cert">Certificate to be supplied</param>
        public ODataReader(string url, X509Certificate cert)
        {
            this.cert = cert;
            this.tempDoc = new XmlDocument();
            this.ns = new DSNameSpaces(this.tempDoc);

            this.StartURI = url;
            this.StartTime = DateTime.Now;
            this.EndTime = DateTime.Now;
            this.ActivityLog = new List<string>();
            this.FileName = Guid.NewGuid().ToString() + ".txt";
            this.SaparatorChar = '|';
            this.IsCompelet = false;
        }

        /// <summary>
        /// Gets or sets ActivityLog log for diagnostic purpose.
        /// </summary>
        public List<string> ActivityLog { get; set; }

        /// <summary>
        /// Gets or sets Startup URL for which download need to started.
        /// </summary>
        public string StartURI { get; set; }

        /// <summary>
        /// Gets or sets Total data rows retrieved by reader.
        /// </summary>
        public long TotalDataRows { get; set; }

        /// <summary>
        /// Gets or sets Total data pages retrieved by reader.
        /// </summary>
        public long TotalPages { get; set; }

        /// <summary>
        /// Gets or sets Reader Start timestamp.
        /// </summary>
        public DateTime StartTime { get; set; }

        /// <summary>
        /// Gets or sets Reader stop timestamp.
        /// </summary>
        public DateTime EndTime { get; set; }
        
        /// <summary>
        /// Gets or sets a value indicating whether download is complete or not.
        /// </summary>
        public bool IsCompelet { get; set; }

        /// <summary>
        /// Gets or sets File name to be used for download.
        /// </summary>
        public string FileName { get; set; }
        
        /// <summary>
        /// Gets or sets Saparator Char to be used.
        /// </summary>
        public char SaparatorChar { get; set; }
        
        /// <summary>
        /// Gets Odata responce type.
        /// </summary>
        public ODATAResponceType ResponceType
        {
            get
            {
                switch (this.stream.LocalName)
                {
                    case "feed":
                        {
                            return ODATAResponceType.Feed;
                        }

                    case "entry":
                        {
                            return ODATAResponceType.Entry;
                        }

                    case "Edmx":
                        {
                            return ODATAResponceType.Metadata;
                        }

                    default:
                        {
                            return ODATAResponceType.Property;
                        }
                }
            }
        }

        /// <summary>
        /// Async version of SaveAsText
        /// </summary>
        /// <param name="fileName">File name.</param>
        /// <param name="saparatorChar">Saparator Char</param>
        public void SaveAsTextAsync(string fileName, char saparatorChar)
        {
            this.FileName = fileName;
            this.SaparatorChar = saparatorChar;
            ThreadPool.QueueUserWorkItem(new WaitCallback(this.SaveAsText));
        }

        /// <summary>
        /// Wrapper over SaveAsText to be used as deligate
        /// </summary>
        /// <param name="stateInfo">Not in use</param>
        public void SaveAsText(object stateInfo = null)
        {
            SaveAsText(this.FileName, this.SaparatorChar);
        }

        /// <summary>
        /// Write output to text file
        /// </summary>
        /// <param name="fileName">File name to write.</param>
        /// <param name="saparatorChar">Saparator char.</param>
        /// <returns>Exception if any.</returns>
        public Exception SaveAsText(string fileName, char saparatorChar)
        {
            Exception exportException = null;
            this.StartTime = DateTime.Now;

            this.AppendActivityLog("Starting download on URL=" + this.StartURI);
            this.Init(this.StartURI);

            XmlElement curNode;

            string[] nodeNames = { "entry" };
            DSNameSpaces ns = new DSNameSpaces(this.tempDoc);
            string propNodeQulifiedName = "//meta:properties";
            TextWriter tw = new StreamWriter(fileName);

            ////flush current node 
            curNode = this.GetNextElementByName(nodeNames);
            if (curNode != null)
            {
                this.AppendActivityLog("Started reading first page.");

                ////Flush Header Info
                tw.WriteLine(this.GetHeaderRow(curNode.SelectSingleNode(propNodeQulifiedName, ns.NSManager), saparatorChar));
                this.AppendActivityLog("Success reading header.");

                //// first data row
                tw.WriteLine(this.GetDataRow(curNode.SelectSingleNode(propNodeQulifiedName, ns.NSManager), saparatorChar));
                this.AppendActivityLog("Success reading first data row.");

                this.TotalPages++;

                ////Parse the stream for either entry or link node 
                nodeNames = new string[] { "entry", "link" };

                //// Flush to rest of nodes

                try
                {
                    while (curNode != null)
                    {
                        ////If current node is next page link then Reset the stream and start over
                        curNode = this.GetNextElementByName(nodeNames);
                        if (curNode != null && this.GetNextPageLink(curNode) != null)
                        {
                            ////flush existing data before starting new page.
                            tw.Flush();
                            string nextPageURL = this.GetNextPageLink(curNode);
                            this.AppendActivityLog("Loading Next Page URL=" + nextPageURL);

                            ////Debug.WriteLine(NextPageURL);
                            this.TotalPages++;

                            this.Init(nextPageURL);

                            ////Posotion stream to first 'entry' node
                            curNode = this.GetNextElementByName(new string[] { "entry" });
                        }

                        ////Flush  till EOF 
                        if (curNode != null)
                        {
                            tw.WriteLine(this.GetDataRow(curNode.SelectSingleNode(propNodeQulifiedName, ns.NSManager), saparatorChar));
                        }
                    }
                }
                catch (Exception e)
                {
                    tw.WriteLine("Exception creating file");
                    tw.WriteLine(e.Message);
                    tw.WriteLine(e.StackTrace);
                    exportException = e;
                }
                finally
                {
                    this.EndTime = DateTime.Now;
                }
            }

            ////If no result found
            if (this.TotalDataRows <= 0)
            {
                tw.WriteLine("No record found");
            }

            tw.Close();

            return exportException;
        }

        /// <summary>
        /// Initializes data members.
        /// </summary>
        /// <param name="uri">OData URL.</param>
        private void Init(string uri)
        {
            if (this.webresp != null)
            {
                this.webresp.Close();
            }

            this.webReq = (HttpWebRequest)WebRequest.Create(uri);
            this.webReq.Timeout = 4 * 60 * 1000;
            if (this.cert != null)
            {
                this.webReq.ClientCertificates.Add(this.cert);
            }

            // throw new ApplicationException(uri);
            this.webresp = (HttpWebResponse)this.webReq.GetResponse();
            this.stream = XmlReader.Create(this.webresp.GetResponseStream());
            this.stream.ReadStartElement();
        }

        /// <summary>
        /// Added AppendActivityLog of reader.
        /// </summary>
        /// <param name="message">message to be looged.</param>
        private void AppendActivityLog(string message)
        {
            if (this.ActivityLog != null)
            {
                this.ActivityLog.Add(message);
                Debug.WriteLine(DateTime.Now.ToString() + message);
            }
        }

        /// <summary>
        /// Gets element by name from responce.
        /// </summary>
        /// <param name="name">Element name</param>
        /// <returns>Xml Element</returns>
        private XmlElement GetNextElementByName(string[] name)
        {
            this.nodeOuterXML = string.Empty;
            this.curLine = string.Empty;
            this.localName = string.Empty;

            while (this.stream.Read())
            {
                this.localName = this.stream.LocalName;
                foreach (string item in name)
                {
                    if (item == this.localName)
                    {
                        this.tempDoc.LoadXml(this.stream.ReadOuterXml());
                        return this.tempDoc.DocumentElement;
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Gets next page link from responce
        /// </summary>
        /// <param name="ele">Odata feed element.</param>
        /// <returns>Next page url.</returns>
        private string GetNextPageLink(XmlElement ele)
        {
            string nextPageURL = null;
            if (ele.LocalName == "link" && ele.OuterXml.Contains("rel=\"next\" "))
            {
                nextPageURL=MapCustomParameters(this.StartURI, ele.Attributes["href"].Value);
                //Debug.WriteLine(string.Format("Found Next Page Link={0}",nextPageURL));
            }

            return nextPageURL;
        }

        /// <summary>
        /// Gets header row from any feed node
        /// </summary>
        /// <param name="dataNode">Xml Date Node</param>
        /// <param name="saparatorChar">Saparator char</param>
        /// <returns>Header row.</returns>
        private string GetHeaderRow(XmlNode dataNode, char saparatorChar)
        {
            string line = string.Empty;
            int propIndex = 0;
            line = dataNode.ChildNodes[propIndex].LocalName;
            if (dataNode.ChildNodes.Count > 1)
            {
                propIndex++;
                do
                {
                    line = line + saparatorChar + dataNode.ChildNodes[propIndex].LocalName;
                    propIndex++;
                }
                while (propIndex < dataNode.ChildNodes.Count);
            }

            return line;
        }

        /// <summary>
        /// Gets data row from feed element.
        /// </summary>
        /// <param name="dataNode">Data node.</param>
        /// <param name="saparatorChar">Saparator char.</param>
        /// <returns>Data row extracted from data node</returns>
        private string GetDataRow(XmlNode dataNode, char saparatorChar)
        {
            string line = string.Empty;
            int propIndex = 0;
            line = dataNode.ChildNodes[propIndex].InnerText;
            if (dataNode.ChildNodes.Count > 1)
            {
                propIndex++;
                do
                {
                    line = line + saparatorChar + dataNode.ChildNodes[propIndex].InnerText;
                    propIndex++;
                }
                while (propIndex < dataNode.ChildNodes.Count);
            }

            if (!string.IsNullOrEmpty(line))
            {
                this.TotalDataRows++;
            }

            return line;
        }
        /// <summary>
        /// Maps Paramater from source url to destination url , adds to destination if missing
        /// </summary>
        /// <param name="originalURL">Source URL.</param>
        /// <param name="nextPageURL">Destination URL.</param>
        /// <returns>New Url with updated parameters.</returns>
        private string MapCustomParameters(string originalURL,string nextPageURL)
        {

            string newURL = string.Empty;
            UrlBuilder builderoriginalURL = new UrlBuilder(originalURL);
            UrlBuilder buildernextPageURL = new UrlBuilder(nextPageURL);
            foreach (string  item in builderoriginalURL.QueryString.Keys)
            {
                if (!buildernextPageURL.QueryString.ContainsKey(item))
                {
                    buildernextPageURL.QueryString.Add(item, buildernextPageURL.QueryString[item]);
                }
            }
            return newURL = buildernextPageURL.ToString();
        }
    }
}
